/*!
 * # Fomantic-UI - API
 * https://github.com/fomantic/Fomantic-UI/
 *
 *
 * Released under the MIT license
 * https://opensource.org/licenses/MIT
 *
 */

(function ($, window, document) {
    'use strict';

    function isWindow(obj) {
        return obj !== null && obj === obj.window;
    }

    function isFunction(obj) {
        return typeof obj === 'function' && typeof obj.nodeType !== 'number';
    }

    window = window !== undefined && window.Math === Math
        ? window
        : globalThis;

    $.fn.api = function (parameters) {
        var
            // use window context if none specified
            $allModules     = isFunction(this)
                ? $(window)
                : $(this),
            time           = Date.now(),
            performance    = [],

            query          = arguments[0],
            methodInvoked  = typeof query === 'string',
            queryArguments = [].slice.call(arguments, 1),
            contextCheck   = function (context, win) {
                var $context;
                if ([window, document].indexOf(context) >= 0) {
                    $context = $(context);
                } else {
                    $context = $(win.document).find(context);
                    if ($context.length === 0) {
                        $context = win.frameElement ? contextCheck(context, win.parent) : window;
                    }
                }

                return $context;
            },
            returnedValue
        ;

        $allModules.each(function () {
            var
                settings          = $.isPlainObject(parameters)
                    ? $.extend(true, {}, $.fn.api.settings, parameters)
                    : $.extend({}, $.fn.api.settings),

                // internal aliases
                regExp          = settings.regExp,
                namespace       = settings.namespace,
                metadata        = settings.metadata,
                selector        = settings.selector,
                error           = settings.error,
                className       = settings.className,

                // define namespaces for modules
                eventNamespace  = '.' + namespace,
                moduleNamespace = 'module-' + namespace,

                // element that creates request
                $module         = $(this),
                $form           = $module.closest(selector.form),

                // context used for state
                $context        = settings.stateContext ? contextCheck(settings.stateContext, window) : $module,

                // request details
                ajaxSettings,
                requestSettings,
                url,
                data,
                requestStartTime,
                originalData,

                // standard module
                element         = this,
                context         = $context[0],
                instance        = $module.data(moduleNamespace),
                module
            ;

            module = {

                initialize: function () {
                    if (!methodInvoked) {
                        originalData = settings.data;
                        module.bind.events();
                    }
                    module.instantiate();
                },

                instantiate: function () {
                    module.verbose('Storing instance of module', module);
                    instance = module;
                    $module
                        .data(moduleNamespace, instance)
                    ;
                },

                destroy: function () {
                    module.verbose('Destroying previous module for', element);
                    $module
                        .removeData(moduleNamespace)
                        .off(eventNamespace)
                    ;
                },

                bind: {
                    events: function () {
                        var
                            triggerEvent = module.get.event()
                        ;
                        if (triggerEvent) {
                            module.verbose('Attaching API events to element', triggerEvent);
                            $module
                                .on(triggerEvent + eventNamespace, module.event.trigger)
                            ;
                        } else if (settings.on === 'now') {
                            module.debug('Querying API endpoint immediately');
                            module.query();
                        }
                    },
                },

                decode: {
                    json: function (response) {
                        if (response !== undefined && typeof response === 'string') {
                            try {
                                response = JSON.parse(response);
                            } catch (e) {
                                // isn't json string
                            }
                        }

                        return response;
                    },
                },

                read: {
                    cachedResponse: function (url) {
                        var
                            response
                        ;
                        if (window.Storage === undefined) {
                            module.error(error.noStorage);

                            return;
                        }
                        response = sessionStorage.getItem(url + module.get.normalizedData());
                        module.debug('Using cached response', url, settings.data, response);
                        response = module.decode.json(response);

                        return response;
                    },
                },
                write: {
                    cachedResponse: function (url, response) {
                        if (window.Storage === undefined) {
                            module.error(error.noStorage);

                            return;
                        }
                        if ($.isPlainObject(response)) {
                            response = JSON.stringify(response);
                        }
                        sessionStorage.setItem(url + module.get.normalizedData(), response);
                        module.verbose('Storing cached response for url', url, settings.data, response);
                    },
                },

                query: function () {
                    if (module.is.disabled()) {
                        module.debug('Element is disabled API request aborted');

                        return;
                    }

                    if (module.is.loading()) {
                        if (settings.interruptRequests) {
                            module.debug('Interrupting previous request');
                            module.abort();
                        } else {
                            module.debug('Cancelling request, previous request is still pending');

                            return;
                        }
                    }

                    // pass element metadata to url (value, text)
                    if (settings.defaultData) {
                        $.extend(true, settings.urlData, module.get.defaultData());
                    }

                    // Add form content
                    if (settings.serializeForm) {
                        settings.data = module.add.formData(originalData || settings.data);
                    }

                    // call beforesend and get any settings changes
                    requestSettings = module.get.settings();

                    // check if before send cancelled request
                    if (requestSettings === false) {
                        module.cancelled = true;
                        module.error(error.beforeSend);

                        return;
                    }

                    module.cancelled = false;

                    // get url
                    url = module.get.templatedURL();

                    if (!url && !module.is.mocked()) {
                        module.error(error.missingURL);

                        return;
                    }

                    // replace variables
                    url = module.add.urlData(url);
                    // missing url parameters
                    if (!url && !module.is.mocked()) {
                        return;
                    }

                    requestSettings.url = settings.base + url;

                    // look for jQuery ajax parameters in settings
                    ajaxSettings = $.extend(true, {}, settings, {
                        type: settings.method || settings.type,
                        data: data,
                        url: settings.base + url,
                        beforeSend: settings.beforeXHR,
                        success: function () {},
                        failure: function () {},
                        complete: function () {},
                    });

                    module.debug('Querying URL', ajaxSettings.url);
                    module.verbose('Using AJAX settings', ajaxSettings);
                    if (settings.cache === 'local' && module.read.cachedResponse(url)) {
                        module.debug('Response returned from local cache');
                        module.request = module.create.request();
                        module.request.resolveWith(context, [module.read.cachedResponse(url)]);

                        return;
                    }

                    if (!settings.throttle) {
                        module.debug('Sending request', data, ajaxSettings.method);
                        module.send.request();
                    } else {
                        if (!settings.throttleFirstRequest && !module.timer) {
                            module.debug('Sending request', data, ajaxSettings.method);
                            module.send.request();
                            module.timer = setTimeout(function () {}, settings.throttle);
                        } else {
                            module.debug('Throttling request', settings.throttle);
                            clearTimeout(module.timer);
                            module.timer = setTimeout(function () {
                                if (module.timer) {
                                    delete module.timer;
                                }
                                module.debug('Sending throttled request', data, ajaxSettings.method);
                                module.send.request();
                            }, settings.throttle);
                        }
                    }
                },

                should: {
                    removeError: function () {
                        return settings.hideError === true || (settings.hideError === 'auto' && !module.is.form());
                    },
                },

                is: {
                    disabled: function () {
                        return $module.filter(selector.disabled).length > 0;
                    },
                    expectingJSON: function () {
                        return settings.dataType === 'json' || settings.dataType === 'jsonp';
                    },
                    form: function () {
                        return $module.is('form') || $context.is('form');
                    },
                    mocked: function () {
                        return settings.mockResponse || settings.mockResponseAsync || settings.response || settings.responseAsync;
                    },
                    input: function () {
                        return $module.is('input');
                    },
                    loading: function () {
                        return module.request
                            ? module.request.state() === 'pending'
                            : false;
                    },
                    abortedRequest: function (xhr) {
                        if (xhr && xhr.readyState !== undefined && xhr.readyState === 0) {
                            module.verbose('XHR request determined to be aborted');

                            return true;
                        }

                        module.verbose('XHR request was not aborted');

                        return false;
                    },
                    validResponse: function (response) {
                        if (!module.is.expectingJSON() || !isFunction(settings.successTest)) {
                            module.verbose('Response is not JSON, skipping validation', settings.successTest, response);

                            return true;
                        }
                        module.debug('Checking JSON returned success', settings.successTest, response);
                        if (settings.successTest(response)) {
                            module.debug('Response passed success test', response);

                            return true;
                        }

                        module.debug('Response failed success test', response);

                        return false;
                    },
                },

                was: {
                    cancelled: function () {
                        return module.cancelled || false;
                    },
                    successful: function () {
                        return module.request && module.request.state() === 'resolved';
                    },
                    failure: function () {
                        return module.request && module.request.state() === 'rejected';
                    },
                    complete: function () {
                        return module.request && (module.request.state() === 'resolved' || module.request.state() === 'rejected');
                    },
                },

                add: {
                    urlData: function (url, urlData) {
                        var
                            requiredVariables,
                            optionalVariables
                        ;
                        if (url) {
                            requiredVariables = url.match(regExp.required);
                            optionalVariables = url.match(regExp.optional);
                            urlData = urlData || settings.urlData;
                            if (requiredVariables) {
                                module.debug('Looking for required URL variables', requiredVariables);
                                $.each(requiredVariables, function (index, templatedString) {
                                    var
                                        // allow legacy {$var} style
                                        variable = templatedString.indexOf('$') !== -1
                                            ? templatedString.slice(2, -1)
                                            : templatedString.slice(1, -1),
                                        value   = $.isPlainObject(urlData) && urlData[variable] !== undefined
                                            ? urlData[variable]
                                            : ($module.data(variable) !== undefined
                                                ? $module.data(variable)
                                                : ($context.data(variable) !== undefined // eslint-disable-line unicorn/no-nested-ternary
                                                    ? $context.data(variable)
                                                    : urlData[variable]))
                                    ;
                                    // remove value
                                    if (value === undefined) {
                                        module.error(error.requiredParameter, variable, url);
                                        url = false;

                                        return false;
                                    }

                                    module.verbose('Found required variable', variable, value);
                                    value = settings.encodeParameters
                                        ? module.get.urlEncodedValue(value)
                                        : value;
                                    url = url.replace(templatedString, value);
                                });
                            }
                            if (optionalVariables) {
                                module.debug('Looking for optional URL variables', requiredVariables);
                                $.each(optionalVariables, function (index, templatedString) {
                                    var
                                        // allow legacy {/$var} style
                                        variable = templatedString.indexOf('$') !== -1
                                            ? templatedString.slice(3, -1)
                                            : templatedString.slice(2, -1),
                                        value   = $.isPlainObject(urlData) && urlData[variable] !== undefined
                                            ? urlData[variable]
                                            : ($module.data(variable) !== undefined
                                                ? $module.data(variable)
                                                : ($context.data(variable) !== undefined // eslint-disable-line unicorn/no-nested-ternary
                                                    ? $context.data(variable)
                                                    : urlData[variable]))
                                    ;
                                    // optional replacement
                                    if (value !== undefined) {
                                        module.verbose('Optional variable Found', variable, value);
                                        url = url.replace(templatedString, value);
                                    } else {
                                        module.verbose('Optional variable not found', variable);
                                        // remove preceding slash if set
                                        url = url.indexOf('/' + templatedString) !== -1
                                            ? url.replace('/' + templatedString, '')
                                            : url.replace(templatedString, '');
                                    }
                                });
                            }
                        }

                        return url;
                    },
                    formData: function (data) {
                        var
                            formData = {},
                            hasOtherData,
                            useFormDataApi = settings.serializeForm === 'formdata'
                        ;
                        data = data || originalData || settings.data;
                        hasOtherData = $.isPlainObject(data);

                        if (useFormDataApi) {
                            formData = new FormData($form[0]);
                            settings.processData = settings.processData !== undefined ? settings.processData : false;
                            settings.contentType = settings.contentType !== undefined ? settings.contentType : false;
                        } else {
                            var
                                formArray = $form.serializeArray(),
                                pushes = {},
                                pushValues = {},
                                build = function (base, key, value) {
                                    base[key] = value;

                                    return base;
                                }
                            ;
                            // add files
                            $.each($('input[type="file"]', $form), function (i, tag) {
                                $.each($(tag)[0].files, function (j, file) {
                                    formArray.push({ name: tag.name, value: file });
                                });
                            });
                            $.each(formArray, function (i, el) {
                                if (!regExp.validate.test(el.name)) {
                                    return;
                                }
                                var
                                    isCheckbox = $('[name="' + el.name + '"]', $form).attr('type') === 'checkbox',
                                    floatValue = parseFloat(el.value),
                                    value = (isCheckbox && el.value === 'on')
                                        || el.value === 'true'
                                        || (String(floatValue) === el.value
                                            ? floatValue
                                            : (el.value === 'false' ? false : el.value)),
                                    nameKeys = el.name.match(regExp.key) || [],
                                    pushKey = el.name.replace(/\[]$/, '')
                                ;
                                if (!(pushKey in pushes)) {
                                    pushes[pushKey] = 0;
                                    pushValues[pushKey] = value;
                                } else if (Array.isArray(pushValues[pushKey])) {
                                    pushValues[pushKey].push(value);
                                } else {
                                    pushValues[pushKey] = [pushValues[pushKey], value];
                                }
                                if (pushKey.indexOf('[]') === -1) {
                                    value = pushValues[pushKey];
                                }

                                while (nameKeys.length > 0) {
                                    var k = nameKeys.pop();

                                    if (k === '' && !Array.isArray(value)) { // foo[]
                                        value = build([], pushes[pushKey]++, value);
                                    } else if (regExp.fixed.test(k)) { // foo[n]
                                        value = build([], k, value);
                                    } else if (regExp.named.test(k)) { // foo; foo[bar]
                                        value = build({}, k, value);
                                    }
                                }
                                formData = $.extend(true, formData, value);
                            });
                        }

                        if (hasOtherData) {
                            module.debug('Extending existing data with form data', data, formData);
                            if (useFormDataApi) {
                                $.each(Object.keys(data), function (i, el) {
                                    formData.append(el, data[el]);
                                });
                                data = formData;
                            } else {
                                data = $.extend(true, {}, data, formData);
                            }
                        } else {
                            module.debug('Adding form data', formData);
                            data = formData;
                        }

                        return data;
                    },
                },

                send: {
                    request: function () {
                        module.set.loading();
                        module.request = module.create.request();
                        if (module.is.mocked()) {
                            module.mockedXHR = module.create.mockedXHR();
                        } else {
                            module.xhr = module.create.xhr();
                        }
                        settings.onRequest.call(context, module.request, module.xhr);
                    },
                },

                event: {
                    trigger: function (event) {
                        module.query();
                        if (event.type === 'submit' || event.type === 'click') {
                            event.preventDefault();
                        }
                    },
                    xhr: {
                        always: function () {
                            // nothing special
                        },
                        done: function (response, textStatus, xhr) {
                            var
                                context            = this,
                                elapsedTime        = Date.now() - requestStartTime,
                                timeLeft           = settings.loadingDuration - elapsedTime,
                                translatedResponse = isFunction(settings.onResponse)
                                    ? (module.is.expectingJSON() && !settings.rawResponse
                                        ? settings.onResponse.call(context, $.extend(true, {}, response))
                                        : settings.onResponse.call(context, response))
                                    : false
                            ;
                            timeLeft = timeLeft > 0
                                ? timeLeft
                                : 0;
                            if (translatedResponse) {
                                module.debug('Modified API response in onResponse callback', settings.onResponse, translatedResponse, response);
                                response = translatedResponse;
                            }
                            if (timeLeft > 0) {
                                module.debug('Response completed early delaying state change by', timeLeft);
                            }
                            setTimeout(function () {
                                if (module.is.validResponse(response)) {
                                    module.request.resolveWith(context, [response, xhr]);
                                } else {
                                    module.request.rejectWith(context, [xhr, 'invalid']);
                                }
                            }, timeLeft);
                        },
                        fail: function (xhr, status, httpMessage) {
                            var
                                context     = this,
                                elapsedTime = Date.now() - requestStartTime,
                                timeLeft    = settings.loadingDuration - elapsedTime
                            ;
                            timeLeft = timeLeft > 0
                                ? timeLeft
                                : 0;
                            if (timeLeft > 0) {
                                module.debug('Response completed early delaying state change by', timeLeft);
                            }
                            setTimeout(function () {
                                if (module.is.abortedRequest(xhr)) {
                                    module.request.rejectWith(context, [xhr, 'aborted', httpMessage]);
                                } else {
                                    module.request.rejectWith(context, [xhr, 'error', status, httpMessage]);
                                }
                            }, timeLeft);
                        },
                    },
                    request: {
                        done: function (response, xhr) {
                            module.debug('Successful API Response', response);
                            if (settings.cache === 'local' && url) {
                                module.write.cachedResponse(url, response);
                                module.debug('Saving server response locally', module.cache);
                            }
                            settings.onSuccess.call(context, response, $module, xhr);
                        },
                        complete: function (firstParameter, secondParameter) {
                            var
                                xhr,
                                response
                            ;
                            // have to guess callback parameters based on request success
                            if (module.was.successful()) {
                                response = firstParameter;
                                xhr = secondParameter;
                            } else {
                                xhr = firstParameter;
                                response = module.get.responseFromXHR(xhr);
                            }
                            module.remove.loading();
                            settings.onComplete.call(context, response, $module, xhr);
                        },
                        fail: function (xhr, status, httpMessage) {
                            var
                                // pull response from xhr if available
                                response     = module.get.responseFromXHR(xhr),
                                errorMessage = module.get.errorFromRequest(response, status, httpMessage)
                            ;
                            if (status === 'aborted') {
                                module.debug('XHR Aborted (Most likely caused by page navigation or CORS Policy)', status, httpMessage);
                                settings.onAbort.call(context, status, $module, xhr);

                                return true;
                            }
                            if (status === 'invalid') {
                                module.debug('JSON did not pass success test. A server-side error has most likely occurred', response);
                            } else if (status === 'error') {
                                if (xhr !== undefined) {
                                    module.debug('XHR produced a server error', status, httpMessage);
                                    // make sure we have an error to display to console
                                    if ((xhr.status < 200 || xhr.status >= 300) && httpMessage !== undefined && httpMessage !== '') {
                                        module.error(error.statusMessage + httpMessage, ajaxSettings.url);
                                    }
                                    settings.onError.call(context, errorMessage, $module, xhr);
                                }
                            }

                            if (settings.errorDuration && status !== 'aborted') {
                                module.debug('Adding error state');
                                module.set.error();
                                if (module.should.removeError()) {
                                    setTimeout(function () { module.remove.error(); }, settings.errorDuration);
                                }
                            }
                            module.debug('API Request failed', errorMessage, xhr);
                            settings.onFailure.call(context, response, $module, xhr);
                        },
                    },
                },

                create: {

                    request: function () {
                        // api request promise
                        return $.Deferred()
                            .always(module.event.request.complete)
                            .done(module.event.request.done)
                            .fail(module.event.request.fail)
                        ;
                    },

                    mockedXHR: function () {
                        var
                            // xhr does not simulate these properties of xhr but must return them
                            textStatus     = false,
                            status         = false,
                            httpMessage    = false,
                            responder      = settings.mockResponse || settings.response,
                            asyncResponder = settings.mockResponseAsync || settings.responseAsync,
                            asyncCallback,
                            response,
                            mockedXHR
                        ;

                        mockedXHR = $.Deferred()
                            .always(module.event.xhr.complete)
                            .done(module.event.xhr.done)
                            .fail(module.event.xhr.fail)
                        ;

                        if (responder) {
                            if (isFunction(responder)) {
                                module.debug('Using specified synchronous callback', responder);
                                response = responder.call(context, requestSettings);
                            } else {
                                module.debug('Using settings specified response', responder);
                                response = responder;
                            }
                            // simulating response
                            mockedXHR.resolveWith(context, [response, textStatus, { responseText: response }]);
                        } else if (isFunction(asyncResponder)) {
                            asyncCallback = function (response) {
                                module.debug('Async callback returned response', response);

                                if (response) {
                                    mockedXHR.resolveWith(context, [response, textStatus, { responseText: response }]);
                                } else {
                                    mockedXHR.rejectWith(context, [{ responseText: response }, status, httpMessage]);
                                }
                            };
                            module.debug('Using specified async response callback', asyncResponder);
                            asyncResponder.call(context, requestSettings, asyncCallback);
                        }

                        return mockedXHR;
                    },

                    xhr: function () {
                        var
                            xhr
                        ;
                        // ajax request promise
                        xhr = $.ajax(ajaxSettings)
                            .always(module.event.xhr.always)
                            .done(module.event.xhr.done)
                            .fail(module.event.xhr.fail)
                        ;
                        module.verbose('Created server request', xhr, ajaxSettings);

                        return xhr;
                    },
                },

                set: {
                    error: function () {
                        module.verbose('Adding error state to element', $context);
                        $context.addClass(className.error);
                    },
                    loading: function () {
                        module.verbose('Adding loading state to element', $context);
                        $context.addClass(className.loading);
                        requestStartTime = Date.now();
                    },
                },

                remove: {
                    error: function () {
                        module.verbose('Removing error state from element', $context);
                        $context.removeClass(className.error);
                    },
                    loading: function () {
                        module.verbose('Removing loading state from element', $context);
                        $context.removeClass(className.loading);
                    },
                },

                get: {
                    normalizedData: function () {
                        return typeof settings.data === 'string' ? settings.data : JSON.stringify(settings.data, Object.keys(settings.data).sort());
                    },
                    responseFromXHR: function (xhr) {
                        return $.isPlainObject(xhr)
                            ? (module.is.expectingJSON()
                                ? module.decode.json(xhr.responseText)
                                : xhr.responseText)
                            : false;
                    },
                    errorFromRequest: function (response, status, httpMessage) {
                        return $.isPlainObject(response) && response.error !== undefined
                            ? response.error // use json error message
                            : (settings.error[status] !== undefined // use server error message
                                ? settings.error[status]
                                : httpMessage);
                    },
                    request: function () {
                        return module.request || false;
                    },
                    xhr: function () {
                        return module.xhr || false;
                    },
                    settings: function () {
                        var
                            runSettings
                        ;
                        runSettings = settings.beforeSend.call($module, settings);
                        if (runSettings) {
                            if (runSettings.success !== undefined) {
                                module.debug('Legacy success callback detected', runSettings);
                                module.error(error.legacyParameters, runSettings.success);
                                runSettings.onSuccess = runSettings.success;
                            }
                            if (runSettings.failure !== undefined) {
                                module.debug('Legacy failure callback detected', runSettings);
                                module.error(error.legacyParameters, runSettings.failure);
                                runSettings.onFailure = runSettings.failure;
                            }
                            if (runSettings.complete !== undefined) {
                                module.debug('Legacy complete callback detected', runSettings);
                                module.error(error.legacyParameters, runSettings.complete);
                                runSettings.onComplete = runSettings.complete;
                            }
                        }
                        if (runSettings === undefined) {
                            module.error(error.noReturnedValue);
                        }
                        if (runSettings === false) {
                            return runSettings;
                        }

                        return runSettings !== undefined
                            ? $.extend(true, {}, runSettings)
                            : $.extend(true, {}, settings);
                    },
                    urlEncodedValue: function (value) {
                        var
                            decodedValue   = window.decodeURIComponent(value),
                            encodedValue   = window.encodeURIComponent(value),
                            alreadyEncoded = decodedValue !== value
                        ;
                        if (alreadyEncoded) {
                            module.debug('URL value is already encoded, avoiding double encoding', value);

                            return value;
                        }
                        module.verbose('Encoding value using encodeURIComponent', value, encodedValue);

                        return encodedValue;
                    },
                    defaultData: function () {
                        var
                            data = {}
                        ;
                        if (!isWindow(element)) {
                            if (module.is.input()) {
                                data.value = $module.val();
                            } else if (!module.is.form()) {
                                data.text = $module.text();
                            }
                        }

                        return data;
                    },
                    event: function () {
                        if (isWindow(element) || settings.on === 'now') {
                            module.debug('API called without element, no events attached');

                            return false;
                        }
                        if (settings.on === 'auto') {
                            if ($module.is('input')) {
                                return element.oninput !== undefined
                                    ? 'input'
                                    : (element.onpropertychange !== undefined
                                        ? 'propertychange'
                                        : 'keyup');
                            }
                            if ($module.is('form')) {
                                return 'submit';
                            }

                            return 'click';
                        }

                        return settings.on;
                    },
                    templatedURL: function (action) {
                        action = action || settings.action || $module.data(metadata.action) || false;
                        url = settings.url || $module.data(metadata.url) || false;
                        if (url) {
                            module.debug('Using specified url', url);

                            return url;
                        }
                        if (action) {
                            module.debug('Looking up url for action', action, settings.api);
                            if (settings.api[action] === undefined && !module.is.mocked()) {
                                module.error(error.missingAction, settings.action, settings.api);

                                return;
                            }
                            url = settings.api[action];
                        } else if (module.is.form()) {
                            url = $module.attr('action') || $context.attr('action') || false;
                            module.debug('No url or action specified, defaulting to form action', url);
                        }

                        return url;
                    },
                },

                abort: function () {
                    var
                        xhr = module.get.xhr()
                    ;
                    if (xhr && xhr.state() !== 'resolved') {
                        module.debug('Cancelling API request');
                        xhr.abort();
                    }
                },

                // reset state
                reset: function () {
                    module.remove.error();
                    module.remove.loading();
                },

                setting: function (name, value) {
                    module.debug('Changing setting', name, value);
                    if ($.isPlainObject(name)) {
                        $.extend(true, settings, name);
                    } else if (value !== undefined) {
                        if ($.isPlainObject(settings[name])) {
                            $.extend(true, settings[name], value);
                        } else {
                            settings[name] = value;
                        }
                    } else {
                        return settings[name];
                    }
                },
                internal: function (name, value) {
                    if ($.isPlainObject(name)) {
                        $.extend(true, module, name);
                    } else if (value !== undefined) {
                        module[name] = value;
                    } else {
                        return module[name];
                    }
                },
                debug: function () {
                    if (!settings.silent && settings.debug) {
                        if (settings.performance) {
                            module.performance.log(arguments);
                        } else {
                            module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
                            module.debug.apply(console, arguments);
                        }
                    }
                },
                verbose: function () {
                    if (!settings.silent && settings.verbose && settings.debug) {
                        if (settings.performance) {
                            module.performance.log(arguments);
                        } else {
                            module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
                            module.verbose.apply(console, arguments);
                        }
                    }
                },
                error: function () {
                    if (!settings.silent) {
                        module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
                        module.error.apply(console, arguments);
                    }
                },
                performance: {
                    log: function (message) {
                        var
                            currentTime,
                            executionTime,
                            previousTime
                        ;
                        if (settings.performance) {
                            currentTime = Date.now();
                            previousTime = time || currentTime;
                            executionTime = currentTime - previousTime;
                            time = currentTime;
                            performance.push({
                                Name: message[0],
                                Arguments: [].slice.call(message, 1) || '',
                                // 'Element'        : element,
                                'Execution Time': executionTime,
                            });
                        }
                        clearTimeout(module.performance.timer);
                        module.performance.timer = setTimeout(function () { module.performance.display(); }, 500);
                    },
                    display: function () {
                        var
                            title = settings.name + ':',
                            totalTime = 0
                        ;
                        time = false;
                        clearTimeout(module.performance.timer);
                        $.each(performance, function (index, data) {
                            totalTime += data['Execution Time'];
                        });
                        title += ' ' + totalTime + 'ms';
                        if (performance.length > 0) {
                            console.groupCollapsed(title);
                            if (console.table) {
                                console.table(performance);
                            } else {
                                $.each(performance, function (index, data) {
                                    console.log(data.Name + ': ' + data['Execution Time'] + 'ms');
                                });
                            }
                            console.groupEnd();
                        }
                        performance = [];
                    },
                },
                invoke: function (query, passedArguments, context) {
                    var
                        object = instance,
                        maxDepth,
                        found,
                        response
                    ;
                    passedArguments = passedArguments || queryArguments;
                    context = context || element;
                    if (typeof query === 'string' && object !== undefined) {
                        query = query.split(/[ .]/);
                        maxDepth = query.length - 1;
                        $.each(query, function (depth, value) {
                            var camelCaseValue = depth !== maxDepth
                                ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
                                : query
                            ;
                            if ($.isPlainObject(object[camelCaseValue]) && (depth !== maxDepth)) {
                                object = object[camelCaseValue];
                            } else if (object[camelCaseValue] !== undefined) {
                                found = object[camelCaseValue];

                                return false;
                            } else if ($.isPlainObject(object[value]) && (depth !== maxDepth)) {
                                object = object[value];
                            } else if (object[value] !== undefined) {
                                found = object[value];

                                return false;
                            } else {
                                module.error(error.method, query);

                                return false;
                            }
                        });
                    }
                    if (isFunction(found)) {
                        response = found.apply(context, passedArguments);
                    } else if (found !== undefined) {
                        response = found;
                    }
                    if (Array.isArray(returnedValue)) {
                        returnedValue.push(response);
                    } else if (returnedValue !== undefined) {
                        returnedValue = [returnedValue, response];
                    } else if (response !== undefined) {
                        returnedValue = response;
                    }

                    return found;
                },
            };

            if (methodInvoked) {
                if (instance === undefined) {
                    module.initialize();
                }
                module.invoke(query);
            } else {
                if (instance !== undefined) {
                    instance.invoke('destroy');
                }
                module.initialize();
            }
        });

        return returnedValue !== undefined
            ? returnedValue
            : this;
    };
    $.api = $.fn.api;

    $.api.settings = {

        name: 'API',
        namespace: 'api',

        debug: false,
        verbose: false,
        performance: true,

        // object containing all templates endpoints
        api: {},

        // whether to cache responses
        cache: true,

        // whether new requests should abort previous requests
        interruptRequests: true,

        // event binding
        on: 'auto',

        // context for applying state classes
        stateContext: false,

        // duration for loading state
        loadingDuration: 0,

        // whether to hide errors after a period of time
        hideError: 'auto',

        // duration for error state
        errorDuration: 2000,

        // whether parameters should be encoded with encodeURIComponent
        encodeParameters: true,

        // API action to use
        action: false,

        // templated URL to use
        url: false,

        // base URL to apply to all endpoints
        base: '',

        // data that will
        urlData: {},

        // whether to add default data to url data
        defaultData: true,

        // whether to serialize closest form
        // use true to convert complex named keys like a[b][1][c][] into a nested object
        // use 'formdata' for formdata web api
        serializeForm: false,

        // how long to wait before request should occur
        throttle: 0,

        // whether to throttle first request or only repeated
        throttleFirstRequest: true,

        // standard ajax settings
        method: 'get',
        data: {},
        dataType: 'json',

        // mock response
        mockResponse: false,
        mockResponseAsync: false,

        // aliases for mock
        response: false,
        responseAsync: false,

        // whether onResponse should work with response value without force converting into an object
        rawResponse: true,

        // callbacks before request
        beforeSend: function (settings) {
            return settings;
        },
        beforeXHR: function (xhr) {},
        onRequest: function (promise, xhr) {},

        // after request
        onResponse: false, // function(response) { },

        // response was successful, if JSON passed validation
        onSuccess: function (response, $module) {},

        // request finished without aborting
        onComplete: function (response, $module) {},

        // failed JSON success test
        onFailure: function (response, $module) {},

        // server error
        onError: function (errorMessage, $module) {},

        // request aborted
        onAbort: function (errorMessage, $module) {},

        successTest: false,

        // errors
        error: {
            beforeSend: 'The before send function has aborted the request',
            error: 'There was an error with your request',
            exitConditions: 'API Request Aborted. Exit conditions met',
            JSONParse: 'JSON could not be parsed during error handling',
            legacyParameters: 'You are using legacy API success callback names',
            method: 'The method you called is not defined',
            missingAction: 'API action used but no url was defined',
            missingURL: 'No URL specified for api event',
            noReturnedValue: 'The beforeSend callback must return a settings object, beforeSend ignored.',
            noStorage: 'Caching responses locally requires session storage',
            parseError: 'There was an error parsing your request',
            requiredParameter: 'Missing a required URL parameter: ',
            statusMessage: 'Server gave an error: ',
            timeout: 'Your request timed out',
        },

        regExp: {
            required: /{\$*[\da-z]+}/gi,
            optional: /{\/\$*[\da-z]+}/gi,
            validate: /^[_a-z][\w-]*(?:\[[\w-]*])*$/i,
            key: /[\w-]+|(?=\[])/gi,
            push: /^$/,
            fixed: /^\d+$/,
            named: /^[\w-]+$/i,
        },

        className: {
            loading: 'loading',
            error: 'error',
        },

        selector: {
            disabled: '.disabled',
            form: 'form',
        },

        metadata: {
            action: 'action',
            url: 'url',
        },
    };
})(jQuery, window, document);
